package com.specificgroup.jira.plugin.acpro.service.impl;

import com.atlassian.jira.bc.EntityNotFoundException;
import com.atlassian.jira.bc.project.component.ProjectComponent;
import com.atlassian.jira.bc.project.component.ProjectComponentManager;
import com.atlassian.jira.issue.CustomFieldManager;
import com.atlassian.jira.issue.fields.CustomField;
import com.atlassian.jira.project.version.Version;
import com.atlassian.jira.project.version.VersionManager;
import com.atlassian.jira.user.util.UserManager;
import com.opensymphony.user.User;
import com.specificgroup.jira.plugin.acpro.Constants;
import com.specificgroup.jira.plugin.acpro.sync.Sync;
import com.specificgroup.jira.plugin.acpro.sync.Event;
import com.specificgroup.jira.plugin.acpro.exception.AcproException;
import com.specificgroup.jira.plugin.acpro.exception.AcproXmlException;
import com.specificgroup.jira.plugin.acpro.mapping.AcproCategory;
import com.specificgroup.jira.plugin.acpro.mapping.AcproProductVersion;
import com.specificgroup.jira.plugin.acpro.mapping.AcproStatus;
import com.specificgroup.jira.plugin.acpro.mapping.AcproUrgency;
import com.specificgroup.jira.plugin.acpro.mapping.AcproUser;
import com.specificgroup.jira.plugin.acpro.mapping.IssueTypeMappingData;
import com.specificgroup.jira.plugin.acpro.mapping.JiraIssueType;
import com.specificgroup.jira.plugin.acpro.mapping.JiraPriority;
import com.specificgroup.jira.plugin.acpro.mapping.JiraProjectVersion;
import com.specificgroup.jira.plugin.acpro.mapping.JiraStatus;
import com.specificgroup.jira.plugin.acpro.mapping.JiraUser;
import com.specificgroup.jira.plugin.acpro.mapping.NotifyCCUser;
import com.specificgroup.jira.plugin.acpro.mapping.PriorityMappingData;
import com.specificgroup.jira.plugin.acpro.mapping.ProjectVersionMappingData;
import com.specificgroup.jira.plugin.acpro.mapping.StatusMappingData;
import com.specificgroup.jira.plugin.acpro.mapping.UserMappingData;
import com.specificgroup.jira.plugin.acpro.model.DownloadedAttachment;
import com.specificgroup.jira.plugin.acpro.model.Settings;
import com.specificgroup.jira.plugin.acpro.model.acpro.AcproItemInfo;
import com.specificgroup.jira.plugin.acpro.model.acpro.AcproItemsCategoriesResult;
import com.specificgroup.jira.plugin.acpro.model.acpro.AcproItemsPage;
import com.specificgroup.jira.plugin.acpro.model.acpro.AcproLoginData;
import com.specificgroup.jira.plugin.acpro.model.acpro.AcproProject;
import com.specificgroup.jira.plugin.acpro.model.acpro.AcproProjectInfo;
import com.specificgroup.jira.plugin.acpro.model.jira.JiraAttachmentData;
import com.specificgroup.jira.plugin.acpro.model.jira.JiraCommentData;
import com.specificgroup.jira.plugin.acpro.model.jira.JiraImportData;
import com.specificgroup.jira.plugin.acpro.model.jira.JiraIssueData;
import com.specificgroup.jira.plugin.acpro.service.AcproManager;
import com.specificgroup.jira.plugin.acpro.service.AcproService;
import com.specificgroup.jira.plugin.acpro.service.JiraAcproPluginJiraManager;
import com.specificgroup.jira.plugin.acpro.service.JiraAcproSettingsService;
import com.specificgroup.jira.plugin.acpro.util.AcproUtil;
import com.specificgroup.jira.plugin.acpro.util.ObjectsBuilder;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.AttachmentType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.CategoriesType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.CategoryType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.CommentDateTimeType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.CommentType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.CommentsType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.ContentsType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.DateDeadlineType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.DateEnteredType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.DateTimeType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.GetAttachmentResponseType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.GetProjectItemsResponseType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.GetProjectListResponseType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.GetProjectResponseType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.GetUserListResponseType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.ItemCountType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.ItemType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.ItemsType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.ProjectType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.StatusType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.StatusesType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.UrgenciesType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.UrgencyType;
import com.specificgroup.jira.plugin.acpro.xml.response.generated.UserType;
import com.specificgroup.jira.plugin.acpro.xml.settings.generated.JiraAcproSettingsType;
import org.apache.log4j.Logger;
import org.ofbiz.core.entity.GenericValue;

import javax.xml.bind.JAXBElement;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/**
 *
 * @author Ivan Holub
 */
public class AcproManagerImpl implements AcproManager {

    private Logger log = Logger.getLogger(getClass());

    private UserManager userManager;

    private AcproService acproService;

    private ProjectComponentManager projectComponentManager;

    private CustomFieldManager customFieldManager;

    private VersionManager versionManager;

    private JiraAcproSettingsService jiraAcproSettingsService;

    private JiraAcproPluginJiraManager jiraAcproPluginJiraManager;

    public AcproManagerImpl(UserManager userManager, AcproService acproService, ProjectComponentManager projectComponentManager, CustomFieldManager customFieldManager, VersionManager versionManager, JiraAcproSettingsService jiraAcproSettingsService, JiraAcproPluginJiraManager jiraAcproPluginJiraManager) {
        this.userManager = userManager;
        this.acproService = acproService;
        this.projectComponentManager = projectComponentManager;
        this.customFieldManager = customFieldManager;
        this.versionManager = versionManager;
        this.jiraAcproSettingsService = jiraAcproSettingsService;
        this.jiraAcproPluginJiraManager = jiraAcproPluginJiraManager;
    }

    public void buildAcproProjectInfo(Settings settings) throws IOException, AcproException {
        AcproLoginData acproLoginData = settings.getAcproLoginData();
        String projectKey = settings.getAcproProjectKey();
        GetProjectResponseType getProjectResponseType = acproService.makeGetProjectRequest(acproLoginData, projectKey);
        ItemCountType itemCountType = getProjectResponseType.getItemCount();
        settings.getAcproResponse().setProjectResponse(getProjectResponseType);
        if (itemCountType == null) {
            throw new AcproXmlException("ItemCountType is null");
        }
        String itemCount = itemCountType.getCount();
        int itemsCount;
        try {
            itemsCount = Integer.valueOf(itemCount);
        } catch (NumberFormatException e) {
            throw new AcproXmlException("Can't parse item count");
        }

        AcproProjectInfo info = new AcproProjectInfo();
        info.setItemsCount(itemsCount);
        settings.setAcproProjectInfo(info);
    }

    public AcproItemsPage getAcproItemsPage(Settings settings, int pageNumber, int numberOfItemsOnPage) throws IOException, AcproException {
        int startIndex = (pageNumber - 1) * numberOfItemsOnPage;
        AcproLoginData acproLoginData = settings.getAcproLoginData();
        String projectKey = settings.getAcproProjectKey();
        List<ItemType> acproXmlItems = getAcproXmlItems(acproLoginData, projectKey, startIndex, numberOfItemsOnPage);
        List<AcproItemInfo> items = new ArrayList<AcproItemInfo>();
        for (ItemType xmlItem: acproXmlItems) {
            AcproItemInfo item = ObjectsBuilder.buildAcproItemInfo(xmlItem);
            items.add(item);
        }
        AcproItemsPage retValue = new AcproItemsPage();
        retValue.setItems(items);
        retValue.setXmlItems(acproXmlItems);
        return retValue;
    }

    public List<ItemType> getAcproXmlItems(AcproLoginData acproLoginData, String projectKey, int fromIndex, int itemsCount) throws IOException, AcproException {
        int it = (itemsCount + Constants.GET_ITEMS_BATCH_SIZE - 1) / Constants.GET_ITEMS_BATCH_SIZE;
        int lastIt = itemsCount % Constants.GET_ITEMS_BATCH_SIZE;
        if (lastIt == 0) {
            lastIt = Constants.GET_ITEMS_BATCH_SIZE;
        }
        List<ItemType> retValue = new ArrayList<ItemType>();
        for (int i = 0; i < it; i++) {
            String fetchCount;
            if (i == it - 1) {
                fetchCount = String.valueOf(lastIt);
            } else {
                fetchCount = String.valueOf(Constants.GET_ITEMS_BATCH_SIZE);
            }
            String startIndex = String.valueOf(fromIndex + i * Constants.GET_ITEMS_BATCH_SIZE + 1);
            GetProjectItemsResponseType response = acproService.makeGetProjectItemsRequest(acproLoginData, projectKey, startIndex, fetchCount);
            ItemsType itemsType = response.getItems();
            if (itemsType == null) {
                throw new AcproXmlException("ItemsType is null");
            }
            List<ItemType> xmlItems = itemsType.getItem();
            retValue.addAll(xmlItems);
        }
        return retValue;
    }

    public List<AcproProject> getAcproProjects(AcproLoginData acproLoginData) throws IOException, AcproException {
        List<AcproProject> retValue = new ArrayList<AcproProject>();
        GetProjectListResponseType projectsResponse = acproService.makeGetProjectsRequest(acproLoginData);
        List<ProjectType> projects = projectsResponse.getProject();
        for (ProjectType p: projects) {
            AcproProject item = new AcproProject();
            item.setKey(p.getProjectKey());
            item.setName(p.getProjectName());
            retValue.add(item);
        }
        return retValue;
    }

    public void acproLogin(AcproLoginData acproLoginData) throws IOException, AcproException {
        acproService.login(acproLoginData);
    }

    public UserMappingData prepareUserMapping(Settings settings) throws IOException, AcproException {
        AcproLoginData acproLoginData = settings.getAcproLoginData();
        JiraAcproSettingsType loadedSettings = settings.getLoadedSettings();
        GetUserListResponseType getUserListResponseType = acproService.makeUserListRequest(acproLoginData);
        settings.getAcproResponse().setUserListResponse(getUserListResponseType);
        List<ItemType> acproXmlItems = settings.getAcproResponse().getAcproSelectedXmlItems();
        Set<String> usersKeys = new HashSet<String>();
        Set<String> notifyCCSetSorted = new TreeSet<String>();
        Map<String, UserType> usersMap = AcproUtil.buildUsersMap(getUserListResponseType);
        for (ItemType item: acproXmlItems) {
            try {
                String assignedToKey = item.getAssignedToKey();
                if (!AcproUtil.isAcproUserKeyUnassigned(assignedToKey)) {
                    UserType xmlUser = usersMap.get(assignedToKey);
                    if (xmlUser == null) {
                        throw new AcproXmlException("No information about Acpro Assigned To User, user key: " + assignedToKey);
                    } else {
                        usersKeys.add(assignedToKey);
                    }
                }

                String enteredByKey = item.getEnteredByKey();
                UserType xmlUser = usersMap.get(enteredByKey);
                if (xmlUser == null) {
                    throw new AcproXmlException("No information about Acpro Entered By User, user key: " + enteredByKey);
                }
                usersKeys.add(enteredByKey);

                CommentsType commentsType = item.getComments();
                if (commentsType != null) {
                    List<CommentType> comments = commentsType.getComment();
                    for (CommentType comment: comments) {
                        String userKey = comment.getUserKey();
                        UserType xu = usersMap.get(userKey);
                        if (xu == null) {
                            throw new AcproXmlException("No information about Acpro User created comment, user key: " + userKey);
                        }
                        usersKeys.add(userKey);
                    }
                }

                String notifyCC = item.getNotifyCC();
                List<String> tokens = AcproUtil.tokenizeNotifyCC(notifyCC);
                notifyCCSetSorted.addAll(tokens);
            } catch (AcproXmlException e) {
                throw new AcproXmlException("Acpro Item is not valid, Item key: " + item.getItemKey() + ". Cause: " + e.getMessage());
            }
        }

        List<AcproUser> acproUsers = new ArrayList<AcproUser>();
        for (String userKey: usersKeys) {
            UserType xmlUser = usersMap.get(userKey);
            AcproUser acproUser = ObjectsBuilder.buildAcproUser(xmlUser);
            acproUsers.add(acproUser);
        }
        Collections.sort(acproUsers, new Comparator<AcproUser> (){
            public int compare(AcproUser o1, AcproUser o2) {
                return o1.getUserID().compareTo(o2.getUserID());
            }
        });

        UserMappingData userMappingData = new UserMappingData();

        userMappingData.setAcproUsers(acproUsers);

        Map<String, JiraUser> usersMapping = new HashMap<String, JiraUser>();
        Set<User> users = userManager.getAllUsers();
        for (AcproUser acproUser: acproUsers) {
            User user = null;
            //try to find user in loaded settings
            String jiraUserName = jiraAcproSettingsService.findUserMapping(loadedSettings, acproUser.getUserKey());
            if (jiraUserName != null) {
                user = findUserByUserName(users, jiraUserName);
            }
            if (user == null) {
                //try to find user by email
                user = findUserByEmail(users, acproUser.getEMail());
            }
            if (user != null) {
                JiraUser jiraUser = ObjectsBuilder.buildJiraUser(user);
                usersMapping.put(acproUser.getUserKey(), jiraUser);
            }
        }
        userMappingData.setUsersMapping(usersMapping);

        //email -> jira user
        List<NotifyCCUser> notifyCCUsers = new ArrayList<NotifyCCUser>();
        int i = 0;
        for (String email: notifyCCSetSorted) {
            NotifyCCUser user = new NotifyCCUser();
            user.setId(String.valueOf(i));
            user.setEmail(email);
            notifyCCUsers.add(user);
            i++;
        }

        Map<String, JiraUser> notifyCCMapping = new HashMap<String, JiraUser>();
        for (NotifyCCUser notifyCCUser: notifyCCUsers) {
            String email = notifyCCUser.getEmail();
            User user = null;
            //try to find user in loaded settings
            String jiraUserName = jiraAcproSettingsService.findNotifyEmailMapping(loadedSettings, email);
            if (jiraUserName != null) {
                user = findUserByUserName(users, jiraUserName);
            }
            if (user == null) {
                //try to find user by email
                user = findUserByEmail(users, email);
            }
            if (user != null) {
                JiraUser jiraUser = ObjectsBuilder.buildJiraUser(user);
                notifyCCMapping.put(notifyCCUser.getId(), jiraUser);
            }
        }

        userMappingData.setNotifyCCUsers(notifyCCUsers);
        userMappingData.setNotifyCCMapping(notifyCCMapping);

        return userMappingData;
    }

    private User findUserByEmail(Set<User> users, String email) {
        for (User user: users) {
            if (email.equalsIgnoreCase(user.getEmail())) {
                return user;
            }
        }
        return null;
    }

    private User findUserByUserName(Set<User> users, String userName) {
        for (User user: users) {
            if (userName.equals(user.getName())) {
                return user;
            }
        }
        return null;
    }

    public IssueTypeMappingData prepareIssueTypeMapping(Settings settings) throws IOException, AcproXmlException {
        JiraAcproSettingsType loadedSettings = settings.getLoadedSettings();
        List<ItemType> acproXmlItems = settings.getAcproResponse().getAcproSelectedXmlItems();
        AcproItemsCategoriesResult res = getItemsCategories(acproXmlItems);
        Set<String> itemsCategories = res.getCategories();
        boolean itemsWithoutCategoryExist = res.isItemsWithoutCategoryExist();
        Map<String, CategoryType> categoriesMap = buildAcproCategoriesMap(settings.getAcproResponse().getProjectResponse());
        List<AcproCategory> acproCategories = new ArrayList<AcproCategory>();
        for (String categoryKey: itemsCategories) {
            CategoryType xmlCategory = categoriesMap.get(categoryKey);
            if (xmlCategory == null) {
                throw new AcproXmlException("No information about Acpro Category, category key: " + categoryKey);
            }
            AcproCategory category = ObjectsBuilder.buildAcproCategory(xmlCategory);
            acproCategories.add(category);
        }
        IssueTypeMappingData retValue = new IssueTypeMappingData();
        Map<String, JiraIssueType> issueTypesMapping = new HashMap<String, JiraIssueType>();
        Map<String, JiraIssueType> map = jiraAcproPluginJiraManager.getJiraIssueTypesMap();
        for (AcproCategory acproObj: acproCategories) {
            String acproKey = acproObj.getKey();
            String jiraId = jiraAcproSettingsService.findIssueTypeMapping(loadedSettings, acproKey);
            if (jiraId != null) {
                JiraIssueType obj =  map.get(jiraId);
                if (obj != null) {
                    issueTypesMapping.put(acproKey, obj);
                }
            }
        }
        retValue.setAcproCategories(acproCategories);
        retValue.setIssueTypesMapping(issueTypesMapping);
        retValue.setItemsWithoutCategoryExist(itemsWithoutCategoryExist);
        if (itemsWithoutCategoryExist) {
            String jiraIssueTypeId = jiraAcproSettingsService.findIssueTypeCategoryIsNotSetMapping(loadedSettings);
            if (jiraIssueTypeId != null) {
                JiraIssueType jit = map.get(jiraIssueTypeId);
                if (jit != null) {
                    retValue.setDefaultJiraIssueTypeId(jiraIssueTypeId);
                    retValue.setDefaultJiraIssueType(jit);
                }
            }
        }
        return retValue;
    }

    private Map<String, CategoryType> buildAcproCategoriesMap(GetProjectResponseType projectResponse) throws AcproXmlException {
        CategoriesType categoriesType = projectResponse.getCategories();
        if (categoriesType == null) {
            throw new AcproXmlException("CategoriesType is null");
        }
        Map<String, CategoryType> retValue = new HashMap<String, CategoryType>();
        for (CategoryType item: categoriesType.getCategory()) {
            retValue.put(item.getKey(), item);
        }
        return retValue;
    }

    public PriorityMappingData preparePriorityMapping(Settings settings) throws IOException, AcproException {
        JiraAcproSettingsType loadedSettings = settings.getLoadedSettings();
        List<ItemType> acproXmlItems = settings.getAcproResponse().getAcproSelectedXmlItems();
        Set<String> itemsUrgencies = getItemsUrgencies(acproXmlItems);
        GetProjectResponseType projectResponse = settings.getAcproResponse().getProjectResponse();
        UrgenciesType urgenciesType = projectResponse.getUrgencies();
        if (urgenciesType == null) {
            throw new AcproXmlException("UrgenciesType is null");
        }
        List<UrgencyType> xmlUrgencies = urgenciesType.getUrgency();
        List<AcproUrgency> acproUrgencies = new ArrayList<AcproUrgency>();
        for (UrgencyType xmlUrgency: xmlUrgencies) {
            String key = xmlUrgency.getKey();
            if (itemsUrgencies.contains(key)) {
                AcproUrgency urgency = ObjectsBuilder.buildAcproUrgency(xmlUrgency);
                acproUrgencies.add(urgency);
            }
        }
        PriorityMappingData retValue = new PriorityMappingData();
        Map<String, JiraPriority> prioritiesMapping = new HashMap<String, JiraPriority>();
        Map<String, JiraPriority> map = jiraAcproPluginJiraManager.getJiraPrioritiesMap();
        for (AcproUrgency acproObj: acproUrgencies) {
            String acproKey = acproObj.getKey();
            String jiraId = jiraAcproSettingsService.findPriorityMapping(loadedSettings, acproKey);
            if (jiraId != null) {
                JiraPriority obj =  map.get(jiraId);
                if (obj != null) {
                    prioritiesMapping.put(acproKey, obj);
                }
            }
        }
        retValue.setAcproUrgencies(acproUrgencies);
        retValue.setPrioritiesMapping(prioritiesMapping);
        return retValue;
    }

    public StatusMappingData prepareStatusMappingData(Settings settings) throws IOException, AcproException {
        JiraAcproSettingsType loadedSettings = settings.getLoadedSettings();
        List<ItemType> acproXmlItems = settings.getAcproResponse().getAcproSelectedXmlItems();
        Set<String> itemsStatuses = getItemsStatuses(acproXmlItems);
        GetProjectResponseType projectResponse = settings.getAcproResponse().getProjectResponse();
        StatusesType statusesType = projectResponse.getStatuses();
        if (statusesType == null) {
            throw new AcproXmlException("StatusesType is null");
        }
        List<StatusType> xmlStatuses = statusesType.getStatus();
        List<AcproStatus> acproStatuses = new ArrayList<AcproStatus>();
        for (StatusType xmlStatus: xmlStatuses) {
            String key = xmlStatus.getKey();
            if (itemsStatuses.contains(key)) {
                AcproStatus status = ObjectsBuilder.buildAcproStatus(xmlStatus);
                acproStatuses.add(status);
            }
        }
        StatusMappingData retValue = new StatusMappingData();
        Map<String, JiraStatus> statusesMapping = new HashMap<String, JiraStatus>();
        Map<String, JiraStatus> map = jiraAcproPluginJiraManager.getJiraStatusesMap();
        for (AcproStatus acproObj: acproStatuses) {
            String acproKey = acproObj.getKey();
            String jiraId = jiraAcproSettingsService.findStatusMapping(loadedSettings, acproKey);
            if (jiraId != null) {
                JiraStatus obj =  map.get(jiraId);
                if (obj != null) {
                    statusesMapping.put(acproKey, obj);
                }
            }
        }
        retValue.setAcproStatuses(acproStatuses);
        retValue.setStatusesMapping(statusesMapping);
        return retValue;
    }

    private AcproItemsCategoriesResult getItemsCategories(List<ItemType> xmlItems) {
        Set<String> categories = new HashSet<String>();
        boolean itemsWithoutCategoryExist = false;
        for (ItemType item: xmlItems) {
            String categoryKey = item.getCategoryKey();
            if (AcproUtil.isAcproCategoryNotSet(categoryKey)) {
                itemsWithoutCategoryExist = true;
            } else {
                categories.add(categoryKey);                    
            }
        }
        AcproItemsCategoriesResult retValue = new AcproItemsCategoriesResult();
        retValue.setCategories(categories);
        retValue.setItemsWithoutCategoryExist(itemsWithoutCategoryExist);
        return retValue;
    }

    private Set<String> getItemsUrgencies(List<ItemType> xmlItems) {
        Set<String> retValue = new HashSet<String>();
        for (ItemType item: xmlItems) {
            retValue.add(item.getUrgencyKey());
        }
        return retValue;
    }

    private Set<String> getItemsStatuses(List<ItemType> xmlItems) {
        Set<String> retValue = new HashSet<String>();
        for (ItemType item: xmlItems) {
            retValue.add(item.getStatusKey());
        }
        return retValue;
    }

    private void doImport(Sync sync, Settings settings) throws AcproException, IOException {
        Event event = new Event(Event.MESSAGE);
        event.setMessage("Start import.");
        sync.addEvent(event);

        //TODO validate other jira objects

        event = new Event(Event.MESSAGE);
        event.setMessage("Validate Acpro data.");
        sync.addEvent(event);

        JiraImportData jiraImportData = makeJiraImportData(settings);

        validateJiraImportData(jiraImportData);

        event = new Event(Event.TOTAL_SIZE);
        long totalSize = calculateAttachmentsSize(jiraImportData);
        event.setSize(totalSize);
        sync.addEvent(event);

        Map<String, Map<String, DownloadedAttachment>> downloadedAttachments = downloadAttachments(sync, settings, jiraImportData);

        List<JiraIssueData> issues = jiraImportData.getIssues();

        event = new Event(Event.TOTAL_ISSUES);
        event.setSize(Long.valueOf(issues.size()));
        sync.addEvent(event);

        for (JiraIssueData issue: issues) {
            jiraAcproPluginJiraManager.createIssue(sync, jiraImportData, issue, downloadedAttachments);
        }

        clearTempFiles(downloadedAttachments);

        event = new Event(Event.FINISH);
        event.setMessage("Import finished successfully.");
        sync.addEvent(event);
    }

    private long calculateAttachmentsSize(JiraImportData jiraImportData) {
        long retValue = 0;
        List<JiraIssueData> issues = jiraImportData.getIssues();
        for (JiraIssueData issue: issues) {
            List<JiraAttachmentData> attachments = issue.getAttachments();
            if (attachments != null) {
                for (JiraAttachmentData att: attachments) {
                    retValue += att.getSizeBytes();
                }
            }
        }
        return retValue;
    }

    private void validateJiraImportData(JiraImportData jiraImportData) throws AcproException {
        List<JiraIssueData> issues = jiraImportData.getIssues();
        Set<String> keys = new HashSet<String>();
        for (JiraIssueData issue: issues) {
            String key = issue.getAcproItemKey();
            if (keys.contains(key)) {
                throw new AcproException("Duplicate Acpro items, key: " + key);
            }
            keys.add(key);
        }
    }

    private void clearTempFiles(Map<String, Map<String, DownloadedAttachment>> downloadedAttachments) {
        for (String itemKey: downloadedAttachments.keySet()) {
            Map<String, DownloadedAttachment> map = downloadedAttachments.get(itemKey);
            if (map != null) {
                for (String filename: map.keySet()) {
                    File tempFile = map.get(filename).getFile();
                    tempFile.delete();
                }
            }
        }
    }

    private JiraImportData makeJiraImportData(Settings settings) throws AcproException {
        JiraImportData retValue = new JiraImportData();

        Long jiraProjectId = settings.getJiraProjectId();
        retValue.setProjectId(jiraProjectId);

        CustomField acproItemNumberCustomField = customFieldManager.getCustomFieldObjectByName(Constants.ACPRO_ITEM_NUMBER_CUSTOM_FIELD_NAME);

        retValue.setAcproItemNumberCustomField(acproItemNumberCustomField);

        List<Long> jiraProjectComponentsIds = settings.getJiraProjectComponentsIds();
        if (jiraProjectComponentsIds != null) {
            try {
                List<ProjectComponent> jiraProjectComponents = projectComponentManager.getComponents(jiraProjectComponentsIds);
                //TODO deprecated                
                Collection<GenericValue> jiraProjectComponentsGeneric = projectComponentManager.convertToGenericValues(jiraProjectComponents);

                retValue.setProjectComponentsGeneric(jiraProjectComponentsGeneric);
                
            } catch (EntityNotFoundException e) {
                //must not occur
                log.error("Error", e);
                throw new RuntimeException(e);
            }
        }

        List<JiraIssueData> issues = new ArrayList<JiraIssueData>();
        List<ItemType> acproItems = settings.getAcproResponse().getAcproSelectedXmlItems();
        for (ItemType acproItem: acproItems) {
            try {
                JiraIssueData data = makeJiraIssueData(acproItem, settings);
                issues.add(data);
            } catch (AcproException e) {
                throw new AcproException("Acpro xml Item " + acproItem.getItemKey() + " is not valid. Cause: " + e.getMessage(), e);
            }
        }
        retValue.setIssues(issues);
        return retValue;
    }

    private JiraIssueData makeJiraIssueData(ItemType acproItem, Settings settings) throws AcproException {
        Long jiraProjectId = settings.getJiraProjectId();
        Map<String, String> usersMapping = settings.getUsersMapping(); //acpro userKey -> jira userName
        Map<String, String> notifyCCMapping = settings.getNotifyCCMapping(); //acpro notifyCC email -> jira userName
        Map<String, String> issueTypeMapping = settings.getIssueTypeMapping(); //acpro category key -> jira issue type id
        Map<String, String> priorityMapping = settings.getPriorityMapping(); //acpro urgency key -> jira priority id
        Map<String, String> statusMapping = settings.getStatusMapping(); //acpro status key -> jira status id
        Map<String, Long> projectVersionMapping = settings.getProjectVersionMapping(); //acpro Product Version Name -> jira Project Version Id

        JiraIssueData data = new JiraIssueData();

        //entered by -> issue creator
        String enteredByKey = acproItem.getEnteredByKey();
        String jiraIssueCreatorUserName = usersMapping.get(enteredByKey);
        User issueCreator = userManager.getUser(jiraIssueCreatorUserName);
        data.setIssueCreator(issueCreator);

        //entered by -> reporter
        data.setReporter(issueCreator);

        //assigned -> assignee
        String acproAssignedUserKey = acproItem.getAssignedToKey();
        if (!AcproUtil.isAcproUserKeyUnassigned(acproAssignedUserKey)) {
            String jiraAssigneeUserName = usersMapping.get(acproAssignedUserKey);
            User assignee = userManager.getUser(jiraAssigneeUserName);
            data.setAssignee(assignee);
        }

        //title -> summary
        String summary = acproItem.getItemTitle();
        data.setSummary(summary);

        //description -> description
        String description = acproItem.getDescription();
        data.setDescription(description);

        //category -> issue type
        String acproCategoryKey = acproItem.getCategoryKey();
        String issueTypeId;
        if (AcproUtil.isAcproCategoryNotSet(acproCategoryKey)) {
            issueTypeId = settings.getCategoryIsNotSetJiraIssueTypeIdMapping();
        } else {
            issueTypeId = issueTypeMapping.get(acproCategoryKey);            
        }
        data.setIssueTypeId(issueTypeId);

        //urgency -> priority
        String acproUrgencyKey = acproItem.getUrgencyKey();
        String priorityId = priorityMapping.get(acproUrgencyKey);
        data.setPriorityId(priorityId);

        //status -> status
        String acproStatusKey = acproItem.getStatusKey();
        String statusId = statusMapping.get(acproStatusKey);
        jiraAcproPluginJiraManager.validateJiraStatus(statusId, jiraProjectId, issueTypeId);
        data.setStatusId(statusId);

        //estimate hours -> estimate seconds
        String acproEstimateHours = acproItem.getEstimateHours();
        if (acproEstimateHours != null) {
            Long estimate = AcproUtil.parseAcproEstimate(acproEstimateHours);
            data.setEstimate(estimate);
        }

        //notifyCC email -> watcher
        List<String> notifyCCEmails = AcproUtil.tokenizeNotifyCC(acproItem.getNotifyCC());
        Set<String> notifyCCEmailsSet = new HashSet<String>();
        notifyCCEmailsSet.addAll(notifyCCEmails);
        List<User> watchers = new ArrayList<User>();
        for (String email: notifyCCEmailsSet) {
            String userName = notifyCCMapping.get(email);
            User user = userManager.getUser(userName);
            watchers.add(user);
        }
        data.setWatchers(watchers);

        //date entered -> date created
        DateEnteredType dateEntered = acproItem.getDateEntered();
        if (dateEntered == null) {
            throw new AcproXmlException("Acpro xml DateEnteredType is null");
        }
        Date dateCreated = AcproUtil.parseAcproDateEntered(dateEntered);
        data.setDateCreated(dateCreated);

        //date dead line -> due date
        DateDeadlineType dateDeadline = acproItem.getDateDeadline();
        if (dateDeadline != null) {
            Date dueDate = AcproUtil.parseAcproDateDeadline(dateDeadline);
            data.setDueDate(dueDate);
        }

        //product version -> affected version
        String productVersion = acproItem.getProductVersion();
        if (AcproUtil.isAcproProductVersionSet(productVersion)) {
            Long id = projectVersionMapping.get(productVersion);
            Version affectedVersion = versionManager.getVersion(id);
            data.setAffectedVersion(affectedVersion);
        }

        //resolved in version -> fix version
        String resolvedInVersion = acproItem.getResolvedInVersion();
        if (AcproUtil.isAcproProductVersionSet(resolvedInVersion)) {
            Long id = projectVersionMapping.get(resolvedInVersion);
            Version fixVersion = versionManager.getVersion(id);
            data.setFixVersion(fixVersion);
        }

        //item key (item number) -> jira custom field
        String acproItemKey = acproItem.getItemKey();
        data.setAcproItemKey(acproItemKey);

        //comments
        List<JiraCommentData> comments = makeComments(usersMapping, acproItem);
        data.setComments(comments);

        //attachments
        List<JiraAttachmentData> attachments = makeAttachments(acproItem);
        data.setAttachments(attachments);

        return data;
    }

    private List<JiraAttachmentData> makeAttachments(ItemType acproItem) throws AcproException {
        ItemType.Attachments attachmentsType = acproItem.getAttachments();
        if (attachmentsType == null) {
            return null;
        }
        List<JiraAttachmentData> retValue = new ArrayList<JiraAttachmentData>();
        List<AttachmentType> acproAttachments = attachmentsType.getAttachment();
        for (AttachmentType acproAttachment: acproAttachments) {
            try {
                DateTimeType dateTime = acproAttachment.getDateTime();
                if (dateTime == null) {
                    throw new AcproXmlException("Acpro xml DateTimeType is null.");
                }
                Date createdTime = AcproUtil.parseAcproDateTime(dateTime);
                String filename = acproAttachment.getName();
                String sizeBytesStr = acproAttachment.getSizeBytes();
                Long sizeBytes = AcproUtil.parseAcproSizeBytes(sizeBytesStr);
                if (sizeBytes > 0) {

                    AcproUtil.validateFileName(filename);
                    
                    //sizeBytes="0" are Acpro links
                    JiraAttachmentData attachment = new JiraAttachmentData();
                    attachment.setFilename(filename);
                    attachment.setCreatedTime(createdTime);
                    attachment.setSizeBytes(sizeBytes);
                    retValue.add(attachment);
                }
            } catch (AcproException e) {
                throw new AcproXmlException("Attachment is not valid, attachment filename: " + acproAttachment.getName() + ". Cause: " + e.getMessage(), e);
            }
        }
        return retValue;
    }

    private List<JiraCommentData> makeComments(Map<String, String> usersMapping, ItemType acproItem) throws AcproXmlException {
        CommentsType commentsType = acproItem.getComments();
        if (commentsType == null) {
            return null;
        }
        List<JiraCommentData> retValue = new ArrayList<JiraCommentData>();
        List<CommentType> acproComments = commentsType.getComment();
        int commentNumber = 0;
        for (CommentType acproComment: acproComments) {
            commentNumber++;
            try {
                List<Serializable> content = acproComment.getContent();
                CommentDateTimeType date = null;
                String value = null;
                for (Object obj: content) {
                    if (obj instanceof String) {
                        value = (String) obj;
                    } else if (obj instanceof JAXBElement) {
                        JAXBElement elem = (JAXBElement) obj;
                        Object d = elem.getValue();
                        if (d instanceof CommentDateTimeType) {
                            date = (CommentDateTimeType) d;
                        }
                    }
                }
                if (value == null) {
                    throw new AcproXmlException("CommentType doesn't contain comment text.");
                }
                if (date == null) {
                    throw new AcproXmlException("CommentType doesn't contain comment date.");
                }
                String acproUserKey = acproComment.getUserKey();
                String authorUserName = usersMapping.get(acproUserKey);

                JiraCommentData comment = new JiraCommentData();
                //comment author
                comment.setAuthorUserName(authorUserName);
                //comment body
                comment.setBody(value);
                //coment created date
                Date dateCreated = AcproUtil.parseAcproCommentDateTime(date);
                comment.setDateCreated(dateCreated);
                retValue.add(comment);                
            } catch (AcproXmlException e) {
                throw new AcproXmlException("Comment is not valid, comment number: " + commentNumber + ". Cause: " + e.getMessage(), e);
            }
        }
        return retValue;
    }

    private DownloadedAttachment getAcproAttachment(AcproLoginData acproLoginData, String itemKey, String filename) throws AcproException, IOException {
        GetAttachmentResponseType response = acproService.makeGetAttachmentRequest(acproLoginData, itemKey, filename);
        ContentsType contents = response.getContents();
        if (contents == null) {
            throw new AcproXmlException("Acpro ContentsType is null");
        }
        byte [] value = contents.getValue();
        if (value == null) {
            throw new AcproXmlException("Acpro ContentsType value is null");
        }
        File file = File.createTempFile(Constants.TEMP_FILE_PREFIX, Constants.TEMP_FILE_SUFFIX);
        file.deleteOnExit();
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(value);
        fos.close();
        DownloadedAttachment retValue = new DownloadedAttachment();
        retValue.setContentType(contents.getContentType());
        retValue.setFile(file);
        return retValue;
    }

    private Map<String, Map<String, DownloadedAttachment>> downloadAttachments(Sync sync, Settings settings, JiraImportData jiraImportData) throws AcproException, IOException {
        Event event = new Event(Event.MESSAGE);
        event.setMessage("Downloading attachments.");
        sync.addEvent(event);
        Map<String, Map<String, DownloadedAttachment>> retValue = new HashMap<String, Map<String, DownloadedAttachment>>();
        try {
            AcproLoginData acproLoginData = settings.getAcproLoginData();
            List<JiraIssueData> issues = jiraImportData.getIssues();
            for (JiraIssueData issue: issues) {
                String itemKey = issue.getAcproItemKey();
                List<JiraAttachmentData> attachments = issue.getAttachments();
                if (attachments != null && attachments.size() > 0) {
                    Map<String, DownloadedAttachment> map = new HashMap<String, DownloadedAttachment>();

                    retValue.put(itemKey, map);

                    for (JiraAttachmentData attachment: attachments) {
                        String filename = attachment.getFilename();
                        event = new Event(Event.MESSAGE);
                        event.setMessage("Item: " + issue.getAcproItemKey() + ", file: " + filename);
                        sync.addEvent(event);
                        DownloadedAttachment da = getAcproAttachment(acproLoginData, itemKey, filename);
                        map.put(filename, da);
                        event = new Event(Event.FILE_SIZE);
                        event.setSize(attachment.getSizeBytes());
                        sync.addEvent(event);
                    }
                }
            }
        } catch (IOException e) {
            log.error("Exception occured during downloading attachments", e);
            clearTempFiles(retValue);
            throw e;
        } catch (AcproException e) {
            log.error("Exception occured during downloading attachments", e);
            clearTempFiles(retValue);
            throw e;
        }

        return retValue;
    }

    private Set<String> getItemsProductVersions(List<ItemType> xmlItems) {
        Set<String> retValue = new HashSet<String>();
        for (ItemType item: xmlItems) {
            String productVersion = item.getProductVersion();
            String resolvedInVersion = item.getResolvedInVersion();
            if (AcproUtil.isAcproProductVersionSet(productVersion)) {
                retValue.add(productVersion);
            }
            if (AcproUtil.isAcproProductVersionSet(resolvedInVersion)) {
                retValue.add(resolvedInVersion);
            }
        }
        return retValue;
    }

    public ProjectVersionMappingData prepareProjectVersionMapping(Settings settings) {
        JiraAcproSettingsType loadedSettings = settings.getLoadedSettings();
        List<ItemType> acproXmlItems = settings.getAcproResponse().getAcproSelectedXmlItems();
        Set<String> itemsProductVersion = getItemsProductVersions(acproXmlItems);
        List<AcproProductVersion> productVersions = new ArrayList<AcproProductVersion>();
        int i = 0;
        for (String productVersion: itemsProductVersion) {
            i++;
            AcproProductVersion v = new AcproProductVersion();
            v.setKey(String.valueOf(i));
            v.setName(productVersion);
            productVersions.add(v);
        }
        ProjectVersionMappingData retValue = new ProjectVersionMappingData();
        Map<String, JiraProjectVersion> projectVersionMapping = new HashMap<String, JiraProjectVersion>();
        Long projectId = settings.getJiraProjectId();
        List<Version> versions = versionManager.getVersions(projectId);
        Map<Long, JiraProjectVersion> jiraProjectVersionsMap = jiraAcproPluginJiraManager.getJiraProjectVersionsMap(projectId);
        for (AcproProductVersion apv: productVersions) {
            String name = apv.getName();
            JiraProjectVersion jpv = null;
            //try to find jira project version from loaded settings
            Long jiraId = jiraAcproSettingsService.findProjectVersionMapping(loadedSettings, name);
            if (jiraId != null) {
                jpv = jiraProjectVersionsMap.get(jiraId);
            }
            if (jpv == null) {
                //try to find jira project version by name
                Version v = findJiraProjectVersionByName(versions, name);
                if (v != null) {
                    jpv = ObjectsBuilder.buildJiraProjectVersion(v);                    
                }
            }
            if (jpv != null) {
                projectVersionMapping.put(apv.getKey(), jpv);
            }
        }
        retValue.setAcproProductVersions(productVersions);
        retValue.setProjectVersionMapping(projectVersionMapping);
        return retValue;
    }

    private Version findJiraProjectVersionByName(List<Version> versions, String name) {
        for (Version v: versions) {
            if (name.equals(v.getName())) {
                return v;
            }
        }
        return null;
    }

    public void setAcproSelectedItems(Settings settings, Map<String, ItemType> itemsMap) {
        List<ItemType> acproSelectedXmlItems = new ArrayList<ItemType>();
        for (String key: itemsMap.keySet()) {
            ItemType item = itemsMap.get(key);
            acproSelectedXmlItems.add(item);
        }
        Collections.sort(acproSelectedXmlItems, new Comparator<ItemType>() {
            public int compare(ItemType o1, ItemType o2) {
                Long key1 = Long.valueOf(o1.getItemKey());
                Long key2 = Long.valueOf(o2.getItemKey());
                return key1.compareTo(key2);
            }
        });
        settings.getAcproResponse().setAcproSelectedXmlItems(acproSelectedXmlItems);
    }

    public void startImport(final Sync sync, final Settings settings) {
        Thread th = new Thread(new Runnable() {
            public void run() {
                try {
                    doImport(sync, settings);
                } catch (Exception e) {
                    log.error("Error", e);
                    Event event = new Event(Event.ERROR);
                    String message = e.getMessage();
                    if (message == null) {
                        message = e.getClass().getName() + ". See JIRA log for details.";
                    }
                    event.setMessage(message);
                    sync.addEvent(event);
                }
            }
        });
        th.start();
    }
}
